/*
 * Created on 22.11.2003
 */
package de.javakicker;

import java.io.IOException;
import java.util.*;

import javax.xml.transform.TransformerException;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

/**
 * The Class GamePlan.
 *
 * @author Till Krullmann
 */
public class GamePlan
{
	/** represents the day of the event. */
	public class Day
	{
		/** The XML element that contains this day of the event. */
		private Element elem;

		/** Die Liste aller Spiele, die an diesem Spieltag stattfinden. Dies ist eine
		 * <code>List</code> und keine <code>Collection</code>, da die Spiele durch
		 * die ID indiziert sind. */
		private List<Match> matches = new Vector<Match>();

		/**
		 * Erzeugt oder laedt einen Spieltag.
			 * @param elem Ein XML-Element. Dieses muss bereits bestehen und "day" als Tag-Namen
		 *             sowie ein Attribut "id" besitzen.
		 */
		Day(Element elem)
		{
			this.elem = elem;

			for (Node node = elem.getFirstChild(); node != null;
				 node = node.getNextSibling())
			{
				if (node.getNodeType() == Node.ELEMENT_NODE &&
					node.getNodeName() == "match")
					matches.add(new Match( (Element) node));
			}
		}


		/**
		 * Liefert das darunterliegende XML-Element.
		 * @return Das XML-Element, in dem dieser Spieltag gespeichert wird.
		 */
		Element getElement()
		{
			return elem;
		}


		/**
		 * Liefert die ID (=Index) dieses Spieltags. Konvention: Der erste Spieltag
		 * erhaelt die ID 1, danach werden neue IDs aufsteigend vergeben.
		 * @return Die ID des Spieltags.
		 */
		public int getId()
		{
			return Integer.parseInt(elem.getAttribute("id"));
		}


		/**
		 * Liefert das <code>Match</code> mit der entsprechenden ID, das an diesem Spieltag
		 * stattfindet.
		 *
		 * @param id Die ID des Spiels.
		 * @return Das Spiel mit der ID <code>id</code>.
		 * @see GamePlan.Match
		 */
		public Match getMatch(int id)
		{
			return matches.get(id - 1);
		}


		/**
		 * Liefert das <code>Match</code>, an dem ein bestimmtes Team teilnimmt. Dies ist
		 * eindeutig, da jedes Team an jedem Spieltag nur einmal spielen kann.
		 * @param team Das Team, fuer das das Match gefunden werden soll.
		 * @return Das Spiel, an dem <code>team</code> teilnimmt, oder <code>null</code>,
		 *         falls kein solches Spiel existiert.
		 * @see GamePlan.Match
		 */
		public Match getMatchForTeam(Team team)
		{
			Iterator<Match> it = matches.iterator();
			while (it.hasNext())
			{
				Match match = it.next();
				if (match.hasParticipant(team))
					return match;
			}
			return null;
		}


		/**
		 * Liefert die Liste aller <code>Match</code>es.
			 * @return Die Liste der Spiele fuer diesen Spieltag. Die Listenelemente haben jeweils
		 * den Index Match-ID - 1.
		 * @see GamePlan.Match
		 */
		public List<Match> getMatches()
		{
			return matches;
		}


		/**
		 * Liefert die naechste Match-ID.
		 * @return Die ID, die das naechste erzeugte Match erhaelt. Dies ist die hoechste
		 *         vorhandene ID + 1.
		 */
		private int getNextMatchId()
		{
			return matches.size() + 1;
		}


		/**
		 * Fuegt dem Spieltag ein neues <code>Match</code> hinzu.
		 * @param team1 Das erste Team, das an dem Spiel teilnimmt.
		 * @param team2 Das zweite Team, das an dem Spiel teilnimmt.
		 * @return Ein <code>Match</code>-Objekt fuer das neue Spiel.
		 * @see GamePlan.Match
		 */
		Match addMatch(Team team1, Team team2)
		{
			Element matchElem = doc.createElement("match");
			matchElem.setAttribute("id", String.valueOf(getNextMatchId()));
			matchElem.setAttribute("team1", team1.getName());
			matchElem.setAttribute("team2", team2.getName());

			elem.appendChild(matchElem);

			Match newMatch = new Match(matchElem);
			matches.add(newMatch);

			return newMatch;
		}
	}


	/** Repraesentiert ein (geplantes oder gespieltes) Spiel. */
	public class Match
	{
		/** Repraesentiert das Ergebnis eines Spiels. */
		public class Result extends GameResult
		{
			/** Das <code>Match</code>, zu dem dieses Ergebnis gehoert. */
			private Match match;

			/** Erzeugt ein neues Ergebnis.
			 * @param match Das <code>Match</code>, zu dem das Ergebnis gehoert.
			 * @param goals1 Die Anzahl der Tore, die Team 1 erzielt hat.
			 * @param goals2 Die Anzahl der Tore, die Team 2 erzielt hat.
			 */
			public Result(Match match, int goals1, int goals2)
			{
				super(goals1, goals2);
				this.match = match;
			}

			/**
			 * Instantiates a new result.
			 *
			 * @param match the match
			 */
			public Result(Match match)
			{
				this(match, 0, 0);
			}


			/**
			 * Instantiates a new result.
			 *
			 * @param match the match
			 * @param otherResult the other result
			 */
			public Result(Match match, GameResult otherResult)
			{
				this(match, otherResult.goals[0], otherResult.goals[1]);
			}


			/**
			 * Sets the result.
			 *
			 * @param result the new result
			 */
			public void setResult(GameResult result)
			{
				this.goals[0] = result.goals[0];
				this.goals[1] = result.goals[1];
			}


			/** Liefert die Anzahl der Tore, die ein bestimmtes Team erzielt hat.
			 * @param team Das Team, fuer das die Tore ermittelt werden sollten. Es
			 *             sollte bekannt sein, dass <code>team</code> an diesem Spiel
			 *             teilnimmt.
			 * @return Die Anzahl der Tore, oder 0, falls <code>team</code> nicht
			 *         am Spiel teilnimmt.
			 */
			public int getGoals(Team team)
			{
				int teamIndex = match.getParticipantIndex(team);
				if (teamIndex == -1)
					return 0;
				else
					return getGoals(teamIndex);
			}


			/**
			 * Sets the goals.
			 *
			 * @param team the team
			 * @param numGoals the num goals
			 */
			void setGoals(Team team, int numGoals)
			{
				int teamIndex = match.getParticipantIndex(team);
				setGoals(teamIndex, numGoals);
			}


			/**
			 * Adds the goal.
			 *
			 * @param team the team
			 */
			void addGoal(Team team)
			{
				int teamIndex = match.getParticipantIndex(team);
				addGoal(teamIndex);
			}


			/** Ermittelt die Tordifferenz fuer ein Team.
			 * @param team Das Team, fuer das die Differenz ermittelt werden soll. Es
			 *             sollte bekannt sein, dass <code>team</code> an diesem Spiel
			 *             teilnimmt.
				 * @return Die Tordifferenz fuer <code>team</code>, oder 0, falls <code>team</code>
			 *         nicht am Spiel teilnimmt.
			 */
			public int getDifference(Team team)
			{
				int teamIndex = match.getParticipantIndex(team);
				if (teamIndex == -1)
					return 0;
				else
					return getDifference(teamIndex);
			}


			/** Ermittelt die Punktzahl fuer ein Team.
			 * @param team Das Team, fuer das die Differenz ermittelt werden soll. Es
			 *             sollte bekannt sein, dass <code>team</code> an diesem Spiel
			 *             teilnimmt.
			 * @return Die Punktzahl fuer das Team. Diese betraegt 3 bei einem Sieg, 1 bei
			 *         einem Unentschieden und 0 bei einer Niederlage oder wenn
			 *         <code>team</code> nicht am Spiel teilnimmt.
			 */
			public int getScore(Team team)
			{
				int teamIndex = match.getParticipantIndex(team);
				if (teamIndex == -1)
					return 0;
				else
					return getScore(teamIndex);
			}


			/** Prueft, ob ein Team das Spiel gewonnen hat.
			 * @param team Ein <code>Team</code>, das an diesem Spiel teilnimmt.
			 * @return <code>true</code>, falls <code>team</code> das Spiel gewonnen hat,
			 *         sonst <code>false</code>.
			 */
			public boolean isWin(Team team)
			{
				return super.isWin(match.getParticipantIndex(team));
			}


			/** Prueft, ob ein Team das Spiel verloren hat.
			 * @param team Ein <code>Team</code>, das an diesem Spiel teilnimmt.
			 * @return <code>true</code>, falls <code>team</code> das Spiel verloren hat,
			 *         sonst <code>false</code>.
			 */
			public boolean isLoss(Team team)
			{
				return super.isLoss(match.getParticipantIndex(team));
			}
		}


		/** Das XML-Element, in dem dieses <code>Match</code> gespeichert wird. */
		private Element elem;

		/** Erzeugt ein neues <code>Match</code>.
		 * @param elem Ein XML-Element. Dieses muss bereits existieren und als Tag-Namen
		 *             "match" und ein Attribut "id" besitzen.
		 */
		Match(Element elem)
		{
			this.elem = elem;
		}


		/** Liefert das darunterliegende XML-Element.
		 * @return Das XML-Element, in dem dieses Spiel gespeichert wird.
		 */
		Element getElement()
		{
			return elem;
		}


		/** Liefert die ID des <code>Match</code>es. Diese Eigenschaft bildet auf das
		 * Attribut "id" des darunterliegenden XML-Elements ab.
		 * @return Die ID des Spiels.
		 */
		public int getId()
		{
			return Integer.parseInt(elem.getAttribute("id"));
		}


		/** Liefert die ID des Spieltags, zu dem dieses Match geh�rt.
		 * @return Die ID des uebergeordneten Spieltags.
		 * @see GamePlan.Day
		 */
		public int getDayId()
		{
			Element dayElem = (Element) elem.getParentNode();
			return Integer.parseInt(dayElem.getAttribute("id"));
		}


		/** Liefert den Teilnehmer-Index eines Teams.
		 * @param team Ein Team
		 * @return 0 oder 1, falls <code>team</code> am Spiel teilnimmt, sonst -1.
		 * @see Team
		 */
		int getParticipantIndex(Team team)
		{
			Team[] participants = getParticipants();
			if (participants[0].equals(team))
				return 0;
			else if (participants[1].equals(team))
				return 1;
			else
				return -1;
		}


		/** Liefert die Namen der teilnehmenden Teams.
		 * @return Ein String-Array mit 2 Eintraegen fuer die Namen der Kontrahenten.
		 */
		public String[] getParticipantNames()
		{
			return new String[]
				{
				elem.getAttribute("team1"), elem.getAttribute("team2")};
		}


		/** Liefert die teilnehmenden Teams.
		 * @return Ein Team-Array mit 2 Eintraegen fuer die Kontrahenten.
		 * @see Team
		 */
		public Team[] getParticipants()
		{
			String[] participantNames = getParticipantNames();
			Team[] participants = new Team[2];

			for (int i = 0; i < 2; i++)
				participants[i] = Team.getTeam(participantNames[i]);

			return participants;
		}


		/** Prueft, ob ein bestimmtes Team an diesem Spiel teilnimmt.
		 * @param team Ein <code>Team</code>.
		 * @return <code>true</code>, falls <code>team</code> an diesem Spiel teilnimmt,
		 *         sonst <code>false</code>.
		 * @see Team
		 */
		public boolean hasParticipant(Team team)
		{
			int index = getParticipantIndex(team);
			return index == 0 || index == 1;
		}


		/** Liefert den Gegner eines Teams.
		 * @param team Ein <code>Team</code>, das an diesem Spiel teilnimmt.
		 * @return Das Team, das au�er <code>team</code> noch an diesem Spiel teilnimmt
		 *         (d.h. der Gegner von <code>team</code>), oder <code>null</code>, falls
		 *         <code>team</code> nicht an diesem Spiel teilnimmt.
		 * @see Team
		 */
		public Team getOpponent(Team team)
		{
			Team[] participants = getParticipants();
			if (participants[0].equals(team))
				return participants[1];
			else if (participants[1].equals(team))
				return participants[0];
			else
				return null;
		}


		/** Liefert das Ergebnis dieses Spiels.
		 * @param roundNumber Die Nummer der Spielrunde.
		 * @return Das Ergebnis des Spiels in der entsprechenden Runde.
		 */
		public Result getResult(int roundNumber)
		{
			for (Node node = elem.getFirstChild(); node != null;
				 node = node.getNextSibling())
				if (node.getNodeType() == Node.ELEMENT_NODE &&
					node.getNodeName() == "result")
				{
					Element resultElem = (Element) node;
					if (Integer.parseInt(resultElem.getAttribute("round")) == roundNumber)
					{
						try
						{
							int goals1 = Integer.parseInt(resultElem.getAttribute(
								"goals1"));
							int goals2 = Integer.parseInt(resultElem.getAttribute(
								"goals2"));
							return new Result(this, goals1, goals2);
						}
						catch (NumberFormatException e)
						{
							return null;
						}
					}
				}

			return null;
		}


		/** Liefert das Ergebnis dieses Spiels aus Sicht eines der Kontrahenten.
		 * @param roundNumber Die Nummer der Spielrunde.
		 * @param team Eines der teilnehmenden Teams.
		 * @return Das Ergebnis aus Sicht von <code>team</code>, oder <code>null</code>,
		 *         falls <code>team</code> nicht an diesem Spiel teilnimmt.
		 */
		public Result getResult(int roundNumber, Team team)
		{
			int participantIndex = getParticipantIndex(team);
			if (participantIndex == -1)
				return null;

			Result result = getResult(roundNumber);

			if (result != null && participantIndex == 1)
				result.swap();

			return result;
		}


		/**
		 * Sets the result.
		 *
		 * @param roundNumber the round number
		 * @param result the result
		 */
		public void setResult(int roundNumber, Result result)
		{
			Element resultElem = null;

			for (Node node = elem.getFirstChild(); node != null;
				 node = node.getNextSibling())
			{
				if ((node.getNodeType() == Node.ELEMENT_NODE) &&
					(node.getNodeName() == "result") &&
					(( (Element) node).getAttribute("round").equals(Integer.toString(roundNumber))))
				{
					resultElem = (Element) node;
				}
			}

			if (result == null)
			{
				if (resultElem != null)
				{
					elem.removeChild(resultElem);
				}
			}
			else
			{
				if (resultElem == null)
				{
					resultElem = doc.createElement("result");
					elem.appendChild(resultElem);
					resultElem.setAttribute("round", Integer.toString(roundNumber));
				}

				resultElem.setAttribute("goals1", Integer.toString(result.getGoals(0)));
				resultElem.setAttribute("goals2", Integer.toString(result.getGoals(1)));
			}
		}
	}


	/** Pfad der Spielplan-Datei. */
	public static final String GAMEPLANFILE = "gameplan.xml";

	/** Das XML-Dokument fuer den Spielplan. */
	private Document doc;

	/** XML-Element fuer den Spielplan (entspricht dem Root-Element von <code>doc</code>). */
	private Element elem;

	/** Die Liste der Spieltage. */
	private List<Day> days = new Vector<Day>();

	/**
	 * Erzeugt einen neuen Spielplan. Dabei wird die Liste der Teams eingelesen und daraus
	 * ein Spielplan generiert. Der fertige Spielplan befindet sich anschliessend nur im
	 * Speicher; er wird nicht in die XML-Datei geschrieben.
	 *
	 * @return the game plan
	 */
	public static GamePlan create()
	{
		Document doc = XmlFile.create();

		Element rootElem = doc.createElement("gameplan");
		doc.appendChild(rootElem);

		GamePlan gamePlan = new GamePlan(doc);
		gamePlan.generate();

		return gamePlan;
	}


	/**
	 * Laedt den Spielplan aus der XML-Datei.
	 *
	 * @return the game plan
	 * @throws IOException Signals that an I/O exception has occurred.
	 * @throws SAXException the sAX exception
	 */
	public static GamePlan load()
		throws IOException, SAXException
	{
		Document doc = XmlFile.readFromFile(GAMEPLANFILE);
		return new GamePlan(doc);
	}


	/** Erzeugt ein neues <code>GamePlan</code> Objekt.
	 * @param doc Das zu Grunde liegende XML-Dokument.
	 */
	private GamePlan(Document doc)
	{
		this.doc = doc;
		elem = doc.getDocumentElement();

		for (Node node = elem.getFirstChild(); node != null; node = node.getNextSibling())
		{
			if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName() == "day")
			{
				days.add(new Day( (Element) node));
			}
		}
	}


	/**
	 * Gets the round count.
	 *
	 * @return Die Anzahl der Spielrunden.
	 */
	public int getRoundCount()
	{
		return Integer.parseInt(elem.getAttribute("round-count"));
	}


	/**
	 * Setzt die Anzahl der Spielrunden.
	 *
	 * @param roundCount the new round count
	 */
	private void setRoundCount(int roundCount)
	{
		elem.setAttribute("round-count", String.valueOf(roundCount));
	}


	/**
	 * Gets the current round.
	 *
	 * @return Die Nummer der aktuellen Spielrunde.
	 */
	public int getCurrentRound()
	{
		return Integer.parseInt(elem.getAttribute("current-round"));
	}


	/**
	 * Setzt die Nummer der aktuellen Spielrunde.
	 *
	 * @param currentRound the new current round
	 */
	private void setCurrentRound(int currentRound)
	{
		elem.setAttribute("current-round", String.valueOf(currentRound));
	}


	/**
	 * Gets the day count.
	 *
	 * @return Die Anzahl der Spieltage.
	 */
	public int getDayCount()
	{
		return days.size();
	}


	/**
	 * Gets the current day id.
	 *
	 * @return Die ID des aktuellen Spieltags.
	 */
	public int getCurrentDayId()
	{
		return Integer.parseInt(elem.getAttribute("current-day"));
	}


	/**
	 * Legt fest, welcher Spieltag der aktuelle ist.
	 *
	 * @param currentDayId the new current day id
	 */
	private void setCurrentDayId(int currentDayId)
	{
		elem.setAttribute("current-day", String.valueOf(currentDayId));
	}


	/**
	 * Gets the day.
	 *
	 * @param dayId the day id
	 * @return Der Spieltag mit der entsprechenden ID.
	 */
	public Day getDay(int dayId)
	{
		return days.get(dayId - 1);
	}


	/**
	 * Gets the current day.
	 *
	 * @return Der aktuelle Spieltag.
	 */
	public Day getCurrentDay()
	{
		return getDay(getCurrentDayId());
	}


	/** Fuegt eine neue Spielrunde an. */
	public void appendRound()
	{
		setRoundCount(getRoundCount() + 1);
	}


	/**
	 * Geht zum naechsten Spieltag ueber.
	 *
	 * @return true, if successful
	 */
	public boolean nextDay()
	{
		int dayId = getCurrentDayId();
		if (dayId >= getDayCount())
			return false;

		setCurrentDayId(dayId + 1);
		return true;
	}


	/** Geht zur naechsten Spielrunde ueber. */
	public void nextRound()
	{
		setCurrentRound(getCurrentRound() + 1);
		setCurrentDayId(1);
	}


	/**
	 * Liefert die ID, die der naechste erzeugte Spieltag erhaelt.
	 *
	 * @return the next day id
	 */
	private int getNextDayId()
	{
		return days.size() + 1;
	}


	/**
	 * Fuegt einen neuen Spieltag hinzu.
	 *
	 * @return the day
	 */
	private Day addDay()
	{
		Element dayElem = doc.createElement("day");
		dayElem.setAttribute("id", String.valueOf(getNextDayId()));

		elem.appendChild(dayElem);

		Day newDay = new Day(dayElem);
		days.add(newDay);

		return newDay;
	}


	/**
	 * Liefert die Liste aller Spieltage.
	 *
	 * @return the days
	 */
	public List<Day> getDays()
	{
		return days;
	}


	/** Liefert die Punktzahl fuer ein bestimmtes Team in einer bestimmten Spielrunde.
	 * @param team Das <code>Team</code>, fuer das die Punktzahl ermittelt werden soll.
	 * @param roundNumber Die Nummer der Spielrunde.
	 * @return Die Punktzahl des Teams in der entsprechenden Runde.
	 */
	public int getTeamScore(Team team, int roundNumber)
	{
		int score = 0;

		Iterator<Day> itDay = days.iterator();
		while (itDay.hasNext())
		{
			Day day = itDay.next();
			Match match = day.getMatchForTeam(team);
			if (match != null)
			{
				Match.Result result = match.getResult(roundNumber);
				score += result.getScore(team);
			}
		}

		return score;
	}


	/** Liefert die Punktzahl fuer ein bestimmtes Team in der aktuellen Spielrunde.
	 * @param team Das <code>Team</code>, fuer das die Punktzahl ermittelt werden soll.
	 * @return Die Punktzahl des Teams in der aktuellen Runde.
	 */
	public int getTeamScore(Team team)
	{
		return getTeamScore(team, getCurrentRound());
	}


	/**
	 * Speichert den Spielplan in der XML-Datei ab.
	 *
	 * @throws TransformerException the transformer exception
	 */
	public void save()
		throws TransformerException
	{
		XmlFile.writeToFile(doc, GAMEPLANFILE);
	}


	/**
		 * Hilfsmethode: Kehrt die Reihenfolge der Elemente in einer verketteten Liste um.
		 *
		 * @param l the l
		 * @return the linked list
		 */
	private static LinkedList<Team> reverseList(List<Team> l)
	{
		LinkedList<Team> l2 = new LinkedList<Team>();
		Iterator<Team> i = l.iterator();
		while (i.hasNext())
			l2.addFirst(i.next());
		return l2;
	}


	/**
	 * Hilfsmethode: Rotiert eine verkettete Liste nach links.
	 *
	 * @param l the l
	 */
	private static void rotateListLeft(LinkedList<Team> l)
	{
		l.addLast(l.removeFirst());
	}


	/**
	 * Hilfsmethode: Rotiert eine verkettete Liste nach rechts.
	 *
	 * @param l the l
	 */
	private static void rotateListRight(LinkedList<Team> l)
	{
		l.addFirst(l.removeLast());
	}


	/** Generiert einen Spielplan. 
	 * 	
	 * ALGORITHMUS:
	 * Seien die Teams mit Buchstaben bezeichnet, z.B. A,B,C,D,E,F fuer 6 Teams
	 * 1. Falls die Anzahl der Teams ungerade ist, fuege ein Dummy-Team hinzu (s.u.)
	 * 2. choppedTeam := Letztes Team; Entferne das letzte Team
	 * 3. fuer jedes i in (0..numTeams - 2)
	 * 		L1 := Liste der Teams, rotiert um i Stellen nach links
	 * 		L2 := L1 in umgekehrter Reihenfolge
	 * 		Die Spielpaarungen ergeben sich aus den Elementen von L1 und L2 mit gleichem
	 * 		Index. Das Team auf der mittleren Position (d.h. L1[j] == L2[j]) spielt gegen
	 * 		das vorher entfernte Team (choppedTeam).
	 * z.B. fuer i = 2
	 * 		CDEAB
	 *      BAEDC
	 * 		=> Spielpaarungen: AD, BC, EF
	 */

	public void generate()
	{
		while (elem.hasChildNodes())
			elem.removeChild(elem.getFirstChild());
		days.clear();

		elem.setAttribute("round-count", "1");
		elem.setAttribute("current-round", "1");
		elem.setAttribute("current-day", "1");

		int numTeams = Team.getTeams().size();
		LinkedList<Team> teams = new LinkedList<Team>(
			/*randomShuffle( new ArrayList(*/Team.getTeams() /*) )*/);

		Team choppedTeam = null;

		/* Falls die Anzahl der Teams ungerade ist, wird ein null-Team hinzugefuegt.
		 * Der Spielpartner des null-Teams muss in der jeweiligen Runde aussetzen. */
		if (numTeams % 2 == 1)
			++numTeams;
		else
			choppedTeam = teams.removeLast();

		int numDays = numTeams - 1;
		//[cp]int numDays = 1;

		LinkedList<Team> revTeams = reverseList(teams);

		//[cp]
		//Day newDay = addDay();
		for (int i = 0; i < numDays; i++)
		{
			Day newDay = addDay();

			Iterator<Team> i1 = teams.iterator(), i2 = revTeams.iterator();
			while (i1.hasNext() && !i1.equals(i2))
			{
				Team t1 = i1.next(), t2 = i2.next();
				if (t1.equals(t2))
					t2 = choppedTeam;
				if (t2 != null)
					newDay.addMatch(t1, t2);

				if (t2 == choppedTeam)
					break;
			}

			rotateListLeft(teams);
			rotateListRight(revTeams);
		}
	}
}
